Skip to content

Add AIHubMix sync provider - #7042

Open
0genlab wants to merge 14 commits into
anomalyco:devfrom
0genlab:feat/aihubmix-sync
Open

0genlab wants to merge 14 commits into
anomalyco:devfrom
0genlab:feat/aihubmix-sync

Conversation

@0genlab

@0genlab 0genlab commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Adds a fully interface-driven sync adapter for AIHubMix, an aggregator that
relays ~408 LLM routes from 26 model labs. Everything is read from the public
catalog endpoint — no credentials, no per-model overrides in the adapter.

GET https://aihubmix.com/api/v1/models?type=llm

This PR ships the adapter only

An earlier revision carried the 299 generated providers/aihubmix/models/*.toml
files alongside the adapter. That pushed the diff past GitHub's 300-file ceiling,
gh pr diff --patch answered HTTP 406, and pr-reviewer.yml died in its
context step before the agent ever started — so no revision of the branch could
reach a review at all.

The catalog files are now dropped from the PR. Once the adapter merges,
sync-models.yml regenerates them on automation/sync-models-aihubmix, a branch
the reviewer workflow already skips by design. The diff here is 22 files — 5 source, 8 note moves, 8 alias deletions, and sync.md.

Answers to the review findings

toggle alongside an effort list containing none is intentional, not a bug

The reviewer flagged models such as glm-5.2 that publish both
{type = "toggle"} and {type = "effort", values = [..., "none", ...]} as
"contradictory metadata" that the adapter should reconcile. It is neither
contradictory nor a defect in the source — it is the gateway's design, and this
repo already documents it.

AIHubMix is a multi-dialect gateway: the same route is reachable over the
OpenAI-compatible /v1/chat/completions path, the Anthropic-compatible
/v1/messages path, and the Gemini native path. Each dialect has its own
off-switch, and the gateway maps whichever one the caller sends onto the
vendor's real control instead of rejecting it. providers/aihubmix/provider.toml
on dev already spells all four out:

  • Raw Chat — $.reasoning_effort = "none"|"minimal"|…|"xhigh"; "none" disables
  • Raw Messages — $.thinking.type = "enabled"|"disabled"|"adaptive"
  • Raw Gemini — $.generationConfig.thinkingConfig, thinkingBudget: 0 off
  • Raw Responses — $.reasoning.effort

So one model genuinely has two independent ways to turn thinking off, which is
exactly toggle + an effort list containing none. Verified live against
glm-5.2 on 2026-09-11 with a fixed prompt:

request reasoning chars result
reasoning_effort: "none" 0 off
enable_thinking: false 0 off
both together 0 coherent, no error
reasoning_effort: "minimal" 217 tier live
reasoning_effort: "max" 328 tier live
reasoning_effort: "turbo" (bogus) 302 HTTP 200, mapped — not 400

That last row is the point: an unrecognised effort is mapped to the nearest
supported tier rather than erroring, because callers arrive from many SDKs with
many vocabularies. Collapsing the two signals in the adapter would delete a real,
independently reachable control from the catalog.

(An earlier comment of mine on this PR said this shape "is being fixed at the
source". That was my own inference and it was wrong; it is documented, intended
behaviour. Corrected publicly in a later comment.)

Wire path for the toggle — fixed

The reviewer was right that {type = "toggle"} on its own does not tell a caller
which field to send, and that a sync rewrites the file whole and would erase a
hand-written header. translateModel now emits the header itself whenever the
model carries a toggle — the same shape the OpenRouter adapter uses — so the wire
path survives every rewrite instead of depending on a human re-adding it.

Intersecting a relay's options with its lab/peer baseline — declined

The suggestion was to narrow a relay's advertised tiers against what its lab
model or sibling routes advertise. That would put per-model knowledge back into
the adapter — the exact thing this rewrite removed (the developer_id → lab
table and the prefix/suffix strip list). The gateway's tier support is a property
of the gateway, not of the lab: AIHubMix deliberately accepts tiers the upstream
vendor does not name and maps them. An intersection would silently drop tiers
that demonstrably work. Where the endpoint is wrong, the fix belongs on the
endpoint; two such fixes have already shipped there during this PR.

What the interface-driven rewrite removed

The adapter originally landed 232 models, resolving relays to lab metadata with
a hand-maintained developer_id → lab table plus a list of routing prefixes and
suffixes to strip. Both are gone. AIHubMix now serves vendor, variant_of
and open_weights on the endpoint itself, so nothing about a relay is inferred
from its ID or mirrored in this repo any more — the adapter reads the catalog's
own answer. That change is what brings the catalog to 308.

first revision now
aihubmix models in the built catalog 232 308
entries using base_model 217 273
standalone full definitions 15 35
lab mapping 21 hand-maintained developer_id entries vendor from the endpoint
relay → base matching 16 prefixes + 10 suffixes stripped from the ID variant_of from the endpoint
open_weights not served; every standalone create blocked served for 289 of 408

How models are matched to lab metadata

The endpoint answers both halves of the lookup itself:

  • vendor names the lab that built the model (292 of 408 routes).
  • variant_of names the AIHubMix ID this entry is a routing variant of, and
    variant_kind labels it (pricing-tier 26, channel-tier 24,
    mode-preset 18, deprecated-alias 8).

A relay is looked up under its own ID first, then under each variant_of hop,
nearest first. Nearest-first matters: qwen3.8-max-preview is declared a
variant of qwen3.8-max and both are published lab models, so the relay factors
onto the preview it actually serves rather than onto the root of its chain.

Following a declared chain also reaches relays no string rule could have:

relay resolves to why a prefix/suffix rule can't
ox-alpha zhipuai/glm-5.3-flash codename shares no substring with the model
grok-code-fast-1 xai/grok-build-0.1 renamed upstream
cohere-command-a cohere/command-a-03-2025 the date tag is the lab's, not the relay's
grok-4-fast-reasoning xai/grok-4.3 deprecated-alias: the old ID now routes to 4.3

The one thing left in the adapter is VENDOR_LABS, four entries for the labs
the two registries spell differently (zhipu/zhipuai, moonshot/moonshotai,
bytedance/bytedance-seed, meituan-longcat/meituan). It maps namespaces;
no entry in it decides what a model is or which lab built it.

Lookups stay case-folded: AIHubMix lowercases every relay ID while labs keep
their own casing, so minimax-m2 has to reach minimax/MiniMax-M2.

Dated release tags are deliberately left on. gemini-2.5-pro-preview-06-05 is a
pinned snapshot, not google/gemini-2.5-pro — and the endpoint does not declare
it a variant of one, which is now the actual test rather than a judgement call
in the adapter.

A resolved base means the entry inherits open_weights, knowledge, license
and the rest from models/, and records only what actually differs — which is
why 273 of 308 entries are three or four lines long.

Standalone entries are skipped, not guessed

A full catalog entry has to carry release_date, open_weights and both limits
itself. The adapter skips anything that has neither lab metadata to inherit
from nor those fields, and reports it through skippedNotice rather than
inventing a value.

open_weights used to block every standalone create because the endpoint did
not serve it. It now does, for 289 of 408 routes, which is what lets the adapter
create standalone entries at all. The guard also checks limits now: the endpoint
sends max_output: 0 for a ceiling it does not know, and without that check
those models reach the writer and fail ModelBase validation.

What is still uncovered, entirely on the endpoint side:

  • 116 routes carry no vendor, so they cannot be factored
  • 104 date to no release_date
  • 119 state no open_weights
  • 104 quote max_output: 0

Every one of these shrinks as AIHubMix backfills its own catalog. The adapter
needs no change for any of them.

Sentinel values in the source data

Two signals mean "unknown", not "zero" or "huge", and are read as absent so the
field falls through to the base model:

signal count why
max_output: 0 104 a servable model cannot emit zero tokens
max_output >= context_length 36 the context window quoted a second time, leaving no room for the prompt

No value is invented — the field is simply omitted and inherited.

Reasoning options

The endpoint carries an extra default key inside reasoning_options[] that
ReasoningOption does not accept, and two non-standard effort spellings,
no_think and instant. The adapter drops the unknown key and maps the
spellings onto none / minimal, filtering anything else outside
ReasoningEffortValue. These are shape-level normalisations — vocabulary
mapping, not per-model judgement — and all four are reported upstream to
AIHubMix.

Deletion policy

deleteMissing is false and trackMissingModels is true. AIHubMix rotates
routes in and out; a transient absence should not delete a catalog entry. The
sourceID hook returns undefined for retire_stage === "deprecated" so
retired routes are marked, not removed.

Framework changes

Four, all small and all needed by something above:

  • formatToml now emits input_audio / output_audio inside cost tiers. Both
    are already in Cost, but were silently dropped when writing tiered pricing.
  • translateModel's options gain header(id), the leading comment block already
    on the file. authoritativeHeaders replaces that block wholesale, so without
    reading it first a provider that owns its header cannot refresh the part it
    generates without discarding the notes a human wrote around it.
  • trackMissingModels is now settable on its own. It was only ever read
    alongside skipCreates, so a provider that creates most models but skips the
    ones it cannot write produced notices nobody acts on. The missing-model issue
    text is reworded to cover that second reading.
  • normalizeModelSlug is exported from openrouter.ts, which already serves as
    the shared helper module for the other provider adapters. Two more adapters
    (venice.ts, wandb.ts) hold byte-identical private copies; left alone here.

Limits are resolved against the lab window, not just accepted

Three rules, in this order. Each was written because the previous revision got a
real route wrong.

A restatement is a ratio, not a direction. The endpoint quotes an accepted
204800 as 200000 and an accepted 1000000 as 1048576. Neither is the host stating
a different window. A decimal restatement of a binary window loses at most
1000/1024 per K unit, so 1000³/1024³ — three nested unit swaps — is the floor
of what a restatement can explain, and two limits within that floor of each other
are one window spelled twice. Checking only the narrowing side left 20 routes
writing an override that states no difference at all (glm-5.3 recording 1048576
against a lab window of 1000000).

The lab entry is the spelling a restatement resolves to, and the provider
file's own value only where the lab states no such key. Matching the lab lets
factoring drop the key entirely; resolving to whatever the file happens to hold
would pin that spelling forever — qwen3.7-flash carries 991000, which is only
an imprecise way of writing the lab's 1000000. Resolving file-first preserved 10
such overrides, claude-opus-4-8 among them still recording 200000/32000 against
a 1000000/128000 lab window. That one is the finding from review: the endpoint
agrees with the lab there, and the narrowing number was the file's.

Then clamp to the lab's window — a relay cannot serve a wider one than the
model it relays; the window is the model's property and a host can only restrict
it. The clamp runs after the resolution rather than instead of it, because an
endpoint quoting back the same stale ceiling the file already holds resolves to
that number and only a later clamp catches it. grok-4.5 is exactly this: the
file held 1000000 for both limits against a 500000 lab window, and the endpoint
quotes that same 1000000.

Past the two max_output sentinels, 14 routes quote a window wider than their
lab entry's (qwen3.8-2.4t-a95b at 1000000 of 262144, gemma-4-31b-it at 131100
of 32768). All are reported upstream to AIHubMix; where the lab entry is the
stale side, models/ is where that gets corrected. After all three rules, the
factored files write 7 limit keys total — every one a genuine host cap, none
byte-identical to its lab entry.

Headers keep human notes and never outlive their block

authoritativeHeaders replaces the leading block on every sync, so what counts
as "the block" has to be exact in both directions.

  • A superseded wire path is told from a note to keep by what the line opens
    with
    (# Toggle:, # Effort:, # Budget:, # Off is effort — the openings
    AGENTS.md prescribes), not by whether it mentions a field path or the docs
    host. Keying on the substring would also delete lines that merely contain one:
    two files state a wire path together with a dated live test the response cannot
    reproduce. A second statement of the same path costs nothing; a deleted
    verification date cannot be recovered. Matched on the trimmed line, or an
    indented # Toggle: outlives the block it documented.
  • The two lines naming this gateway's wire paths are the adapter's own
    restatement of the block, so they go whether or not a block replaces them. Only
    the docs link survives as a note, and only where no derived block restates it.
    Keeping the wire paths when nothing was derived is what left a route
    advertising a toggle it no longer had — the block vanished, its tail survived
    as a "note", and no later sync could tell the difference, so the file never
    self-corrected.
  • AGENTS.md requires these notes above the first key, since a sync keeps only
    the leading block and drops every comment below it. Eight files in this
    provider carried theirs mid-body; they are moved up here, which is 8 of the 22
    files in this diff. Without the move the next sync deletes them silently.

Display names

The endpoint's label is recorded only where the relay is not that lab model under
other punctuation — its bare ID, normalised, differs from the base model's
slug. Bare, because that is what resolved the base model in the first place
(Qwen/QwQ-32B reaches qwen/qwq-32b); normalising the namespaced form matches
nothing, and each of the 10 namespaced routes would take a redundant storefront
override the moment its lab file lands.

This keeps coding-glm-4.6-free reading "Coding GLM 4.6 (free)" rather than the
bare "GLM-4.6" it would share with two other routes, while entries differing only
in punctuation (GLM 5.3 against the lab's GLM-5.3) defer to the lab and write
nothing. A name already on the file outranks both — four files spell their model
the way its lab does (MiMo-V2.5) where the endpoint sends a storefront
Mimo V2.5 — and handing it through stays correct because factoring drops a name
the lab states identically.

A blank label is not a name. ModelBase.name is min(1), so writing one through
aborts the entire provider's sync at validation rather than skipping the
field. 0 of the 409 live routes send one today, which is exactly why it needed a
test rather than a live check.

Verification

  • bun run validate — exit 0
  • bun test packages/core/test/sync.test.ts — 220 pass, 2 fail (both
    pre-existing on dev: DeepInfra modalities and an LLM Gateway case-variant
    assertion that only fails on case-insensitive filesystems)
  • every rule above is pinned by a test that fails when the rule alone is
    reverted, checked one at a time
  • bunx tsc --noEmit -p packages/core — no aihubmix errors
  • two consecutive real syncs — 129 created, 69 updated then
    0 created, 0 updated, 0 removed with 198 unchanged (idempotent)
  • all 8 moved notes survive a real sync; 0 orphaned wire-path lines left behind
  • built-catalog diff against dev — no provider other than aihubmix changes

Review round: two fixed, two answered

Empty reasoning_options on creates — fixed

A route the endpoint reports as reasoning while publishing no
reasoning_options reached the runner with the field unset, and the runner
reads that on a reasoner as "no caller control" and stamps [] — which
AGENTS.md rule 4 forbids using for uncertainty. The guard meant to prevent it
compares against the base model's options, and ModelMetadataBase has no
reasoning_options field, so that comparison is always undefined and the
guard never fires.

The adapter now throws MissingReasoningOptionsError on the two paths that
actually write, the same as the Cloudflare adapter. 13 creates in the
current listing are skipped and surface as missing models instead of being
published with an invented "no controls". An authored [] already on a file is
a human stating the host exposes no control, and is carried through untouched
(grok-build-0.1 on dev).

Stale deprecated never cleared — fixed

status only ever wrote deprecated and otherwise handed the file's value
back, so a route that came back from retirement kept the mark for good.
retire_stage rides on every route (407 active, 2 deprecated), so it is
authoritative about retirement — and only about retirement. A non-deprecated
stage now clears a deprecated status; alpha and beta are left alone
because the endpoint says nothing about either.

Bare budget_tokens — not a defect

Raised as "inventing unbounded controls that shadow real bounds". Three
reasons it is neither:

  • Not invented. The endpoint publishes 103 bare budget_tokens
    entries itself. The adapter relays a declaration; it does not synthesise one.
  • Nothing is shadowed. ModelMetadata has no reasoning_options field, so
    a lab entry cannot carry a budget range for a provider file to override. A
    first-party entry for the same model states that host's API contract, which
    does not transfer — AIHubMix's Anthropic-compatible path rejects a manual
    budget_tokens on the Opus tier, which the affected files record in their
    headers, so copying min = 1024 across would be wrong.
  • It is the repo's own shape. AGENTS.md's Qwen3.5 Plus example authors
    { type = "budget_tokens" } bare with the wire path in the header comment,
    and 256 of the 354 budget entries under providers/ are bare
    (requesty 138, alibaba 23, neuralwatt 16). Authored min/max on the
    8 aihubmix files that have them are preserved, not replaced.

Copying bounds from a peer provider would also put per-model knowledge back
into the adapter, which is what this rewrite exists to remove.

Deleted channel aliases — no gap

Flagged as deletions into a catalog gap. Each of the 8 dropped alias files is
absent from the model list while the ID it routes to is on it, so every
one is replaced by an entry the first sync writes. The gap is visible only
because this PR ships the adapter without its generated files.

Note for maintainers on the sync gate

aihubmix is not in REVIEWED_REASONING_PROVIDERS, and most of its routes are
reasoning models, so the hourly sync PR will be classified unsafe on nearly
every run and wait for a human. That is the right default for a new adapter —
happy to leave it there until it has a track record.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/aihubmix.ts / packages/core/src/sync/index.ts (preserveReasoningOptions) - Check: reasoning = true ⇒ real host reasoning_options; relays must not use [] from uncertainty (AGENTS.md Reasoning options). Why: On creates, when the endpoint omits reasoning/reasoning_options (PR notes ~107 routes omit reasoning), buildAihubmixModel leaves both unset, then the runner resolves lab reasoning = true and preserveReasoningOptions stamps reasoning_options = []. Lab metadata has no reasoning_options, so that empty array is written on the provider file and means “no caller control,” wiping GPT/Claude/DeepSeek-style controls for every such create. Action: On create, if the resolved model reasons and the endpoint did not yield usable options, skip the model (undefined + missing-model issue) or throw MissingReasoningOptionsError—do not emit []. Only write [] when the host affirmatively has no control.
  • [high] [possible mistake] providers/aihubmix/models/xiaomi-mimo-v2.5.toml / xiaomi-mimo-v2.5-pro.toml (deletions) - Check: Catalog boundary is the main listed ID; do not drop the only local route for a served model. Why: Patch 19 deletes these as “hidden channel aliases,” but this tree has no mimo-v2.5.toml / mimo-v2.5-pro.toml (or other primary stand-ins). Peer providers catalog mimo-v2.5 under that id; aihubmix only had the xiaomi-mimo-v2.5* files. Until automation recreates a listed id, MiMo V2.5 disappears from aihubmix. Same risk for alicloud-/deep-/zai- DeepSeek/GLM deletes while bare deepseek-v4-pro / glm-5.1 are also absent. Action: Confirm each deleted id is unlisted and only aliases a listed id that this PR or the first sync will author. If xiaomi-mimo-v2.5 (etc.) is the listed id, keep it (or add the real listed id here) instead of deleting into a gap.
  • [medium] [possible mistake] packages/core/src/sync/providers/aihubmix.ts (status / retire_stage) - Check: Deprecation should follow the endpoint when it is authoritative for status. Why: status is set to "deprecated" when retire_stage === "deprecated", otherwise existing?.status is kept. A once-deprecated relay that leaves retirement stays deprecated forever (unlike DigitalOcean/DeepInfra, which clear stale deprecation). Action: When retire_stage is present and not deprecated, clear a prior "deprecated" status (keep unrelated alpha/beta if you still preserve those from the file).
  • [medium] [violation] packages/core/src/sync/providers/aihubmix.ts (reasoningOptions / budget_tokens) - Check: budget_tokens bounds only when verified; bare budget must not pretend to be a full control surface when lab/peer hosts document ranges. Why: Creates write endpoint-bare { type = "budget_tokens" } with no min/max. Authored bounds are only merged on update. First-time Claude/Gemini-style creates therefore publish unbounded budget where peers use real ranges (e.g. min 1024), and nothing re-applies bounds until a human edits the file. Action: For creates with bare budget_tokens and no authored range, either omit budget until bounds are known, copy a verified same-host peer range, or skip/issue the model—do not ship unbounded budget as if it were complete host metadata.
  • [low] [possible mistake] PR body / first post-merge sync - Check: Data-changing automation should be reviewable against citations. Why: This PR ships the adapter and deletes routes but defers ~300 generated TOMLs to sync-models-aihubmix, which the reviewer workflow skips—so the bulk of pricing/limits/options never gets the same review. Action: After the first automation run, spot-check a sample of new/updated files (reasoning shapes, costs, primary MiMo/DeepSeek/GLM ids) against the public catalog and lab peers, or temporarily include a representative generated subset in a follow-up PR.

@0genlab

0genlab commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Pushed ef0f2c2ec + 5c0590c84 for this round. Two of the four findings were real; the other two are answered in the description (the reviewer reads the PR body, not comments, so that is where the detail lives).

Fixed

  • Empty reasoning_options on creates. Real, and the root cause is a guard that can never fire: preserveReasoningOptions compares against the base model's options, but ModelMetadataBase has no reasoning_options field, so the comparison is always undefined. 14 files were getting [] stamped on models whose lab entry says reasoning = true. The adapter now throws MissingReasoningOptionsError on the two paths that write — same as cloudflare-ai-gateway.ts — so 13 creates are skipped and surface as missing models instead. An authored [] is a human statement and is left alone.
  • Stale deprecated never cleared. Real. retire_stage is present on every route (407 active, 2 deprecated), so a non-deprecated stage now clears a deprecated status while leaving alpha/beta untouched, following digitalocean.ts:409-411.

Answered, no change

  • Bare budget_tokens is not invented: the endpoint publishes 103 of them, AGENTS.md's own Qwen3.5 Plus example authors the same bare shape with the wire path in the header, and 256 of the 354 budget entries under providers/ are bare. Nothing is shadowed — ModelMetadata cannot state a budget range, and a first-party entry's bounds describe that host's API (AIHubMix's Anthropic-compatible path rejects a manual budget_tokens on the Opus tier, which those files record in their headers).
  • Dropped channel aliases leave no gap: each of the 8 is absent from the model list while the ID it routes to is on it, so each is replaced by an entry the first sync writes. The gap only looks real because this PR ships the adapter without its generated files.

222 pass / 2 fail (both pre-existing on dev), tsc unchanged, validate exit 0, sync 129 created, 69 updated then 0/0/0, and no file newly carries reasoning_options = []. Each fix was reverted in turn and a test failed for it.

chenxue and others added 2 commits September 15, 2026 12:26
Adds a sync provider for AIHubMix, an aggregator relaying ~409 LLM routes from
26 labs. Everything is read from the public catalog endpoint — no credentials,
no per-model tables in the adapter.

  GET https://aihubmix.com/api/v1/models?type=llm

Relay → lab resolution comes from the endpoint itself: `vendor` names the lab,
`variant_of` names the route this entry is a variant of, and lookups walk that
chain nearest-first. The hand-maintained `developer_id → lab` table and the
prefix/suffix strip list are gone; what remains is `VENDOR_LABS`, four entries
mapping namespaces the two registries spell differently.

Shape rules, not per-model judgement:
- `max_output: 0` and `max_output >= context_length` are read as "unknown" and
  inherited from the base model rather than published.
- A stated limit below an accepted one but at or above 1000³/1024³ is a decimal
  restatement of a binary window, not a host cap, and resolves to the accepted
  value.
- Effort spellings outside `ReasoningEffortValue` are mapped or dropped; the
  endpoint's extra `default` key inside `reasoning_options[]` is not part of
  `ReasoningOption` and is dropped.
- A model whose lab entry says `reasoning = true` but which publishes no
  controls is skipped, never stamped with `reasoning_options = []` — an empty
  array means "confirmed no caller control", not "not researched".
- A relay with a named vendor belongs on `base_model`; it is never authored as
  a full standalone entry.

`deleteMissing` is false and `trackMissingModels` is true: AIHubMix rotates
routes in and out, and a transient absence should not delete a catalog entry.
Unresolvable relays open deduped `[missing-model]` issues instead of notices
nobody acts on.

Framework side: `formatToml` emits `input_audio` / `output_audio` inside cost
tiers (both are already in `Cost`, but were dropped when writing tiered
pricing), and `issueModels` also collects skipped remotes when a provider
tracks missing models without skipping creates.

Rebased onto dev and squashed from 22 commits; the review-round history lives
in the PR thread. Two conflicts resolved by union rather than by taking a side:
`issueModels` keeps dev's `missingRemote` + dedupe alongside this branch's
`trackMissingModels` condition, and the missing-model issue body keeps dev's
wording with the `base_model` case appended.

Verification: `bun run validate` exit 0; `bun test packages/core/test/sync.test.ts`
221 pass / 3 fail, the same 3 failing on a clean `origin/dev` checkout
(Hyper reasoning inheritance, DeepInfra modalities, and an LLM Gateway
case-variant assertion that only fails on case-insensitive filesystems).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`-free`, `-reasoning` and `-non-reasoning` all name a way into a model the
listing already carries under its own ID. `-free` is the free-tier route (53
of them, 40 pointing at the paid route through `variant_of`); the two Grok
suffixes are the pre-split routes that reach one model with thinking forced
on or off, which the catalog states as `reasoning_options` rather than as two
entries.

Filtered in parseModels rather than translateModel, so a variant is absent
from the sync altogether — no file, and no skip notice or missing-model issue
asking a human to supply metadata the catalog does not want. The relay catalog
keeps every entry, because a variant is still a valid `variant_of` target for
a route that does belong here.

Removes the four free-tier files already written, and takes the dry run from
129 creates to 106.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/aihubmix.ts:509 - Check: authoritativeHeaders must refresh wire-path headers without dropping required/verified control docs (AGENTS.md toggle header rule; Effort/Budget comments on existing aihubmix reasoners). Why: composeHeader strips every leading line matching # Toggle|Effort|Budget|Off is effort, while reasoningHeader only regenerates a generic multi-dialect toggle/folded block. It never rewrites # Effort: … or # Budget: …. On the first sync, that permanently deletes live-verified headers such as DeepSeek’s # Effort: reasoning_effort = high|max and Qwen’s # Budget: thinking_budget = …, and leaves toggle+effort / toggle+budget files without those wire paths. Action: Regenerate Effort and Budget header lines from the resolved reasoning_options (Friendli-style), or stop classifying # Effort: / # Budget: as disposable when those option types are still present; add tests that existing DeepSeek/Qwen headers keep Effort/Budget docs after a rewrite.
  • [medium] [possible mistake] packages/core/src/sync/providers/aihubmix.ts:172 - Check: Deleted provider routes must be absent from the authoritative AIHubMix main list (or true hidden aliases), not primary listed IDs. Why: The PR removes xiaomi-mimo-v2.5.toml and xiaomi-mimo-v2.5-pro.toml, which are normal non-zero-priced base_model entries (same pattern as many other hosts), while coding-xiaomi-mimo-v2.5* is kept. That is a stronger claim than the alicloud- / deep- / zai- channel aliases. If those Xiaomi IDs are on GET /api/v1/models?type=llm, this drops real catalog routes; if they are only hidden aliases, the deletion is fine. Action: Confirm against the live listing for each deleted ID (especially xiaomi-mimo-v2.5 / xiaomi-mimo-v2.5-pro) and restore any ID that is a primary listed route.
  • [medium] [possible mistake] packages/core/src/sync/providers/aihubmix.ts:391 - Check: Per-model toggle wire comments must name the host controls this route actually uses. Why: TOGGLE_HEADER documents Chat as $.enable_thinking, but providers/aihubmix/provider.toml documents the OpenAI-compatible Chat surface as $.reasoning_effort (with "none" to disable). For effort-shaped routes, the regenerated header can point callers at the wrong Chat field after authoritativeHeaders rewrites the file. Action: Align generated headers with the provider’s documented dialects (include reasoning_effort where effort is published; keep enable_thinking only where that field is the real control), or derive the header from the option types actually emitted for that model.

The endpoint uses `reasoning` to state the controls this host exposes, so
leaving it off is the catalog saying it exposes none. Reading the omission
as unknown and falling back to the lab entry's `true` was what left six
routes unwritable: the lab says the model reasons, the endpoint publishes
no controls for it, and AGENTS.md requires `reasoning_options` whenever
`reasoning = true`, so the route could only be skipped.

Nine routes resolve differently under this rule — the four chat-tuned
snapshots, gemini-2.5-flash-image, the two deprecated MiMo routes, and
three Qwen/Solar entries — and none of them has a file today, so nothing
already published flips. `tool_call` keeps the older reading, because
nothing in the endpoint denies tool use.

Takes the dry run from 13 skips to 7, all of which are now routes the
endpoint really does flag as reasoning without publishing the tiers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/aihubmix.ts:2538 - Check: Relay adapters must not invent reasoning = false from an omitted host flag when the lab model reasons; treat absent API flags as unknown (inherit lab) or skip incomplete reasoners, never stamp false. Why: Patch 3 sets const reasoning = model.reasoning ?? false for every route. The same file states the endpoint never sends false and omits reasoning on ~108/409 routes. On factored creates this writes an explicit reasoning = false override against labs with reasoning = true, and preserveReasoningOptions then drops any published reasoning_options whenever model.reasoning === false. Peer Friendli documents the opposite rule for the same failure mode. That under-reports reasoning on this multi-dialect gateway and can strip real controls when options are present but the flag is omitted (standalone path special-cases options; the factored path does not). Action: Restore tri-state handling for factored relays (true / false / omit→inherit lab), keep skipping or issuing only when the lab/host is a reasoner with no resolvable controls, and only default false on true standalone/host-unique entries. Add a regression that a lab reasoner with omitted reasoning and published options keeps options (or is skipped)—not reasoning = false with options stripped.
  • [medium] [possible mistake] packages/core/src/sync/providers/aihubmix.ts:2329 - Check: Route-variant filters must not drop real model IDs whose names end in the filtered suffix. Why: ROUTE_VARIANT_SUFFIX = /-(?:free|non-reasoning|reasoning)$/i intentionally matches AiHubmix-Phi-4-mini-reasoning, which is Microsoft’s model name (also cataloged as providers/azure/models/phi-4-mini-reasoning.toml), not a Grok pre-split steering alias. The adapter comments that it “writes no file today,” so the filter is currently silent damage, not a safe no-op once that route becomes complete enough to author. Action: Narrow the drop list to free-tier / Grok steering IDs (e.g. require variant_of, known prefixes, or an allowlist), and explicitly exclude *-mini-reasoning / other lab names that embed reasoning.

chenxue and others added 3 commits September 15, 2026 15:39
…own"

This reverts commit 1eeaa0c.

Reading the endpoint's omission as a denial was wrong. The omission is
missing data, not a statement: 108 routes leave the flag off and they
include `Qwen/QwQ-32B`, which has no non-thinking mode at all,
`AiHubmix-Phi-4-mini-reasoning`, `codex-mini-latest`, and the whole
`qwen3-*` hybrid-thinking family. A catalog that omits the flag for a
model that can only think is not describing what it exposes.

The measurement that justified the rule -- nine routes resolve
differently and none has a file today -- sized the blast radius, not
the claim. A real sync shows what it actually writes: six cards carry
`reasoning = false` against a lab entry that says `true`, and for
`mimo-v2-omni` / `mimo-v2-pro` that makes this the only provider in the
repo denying that MiMo v2 reasons, where abacus writes `reasoning =
true` and the three first-party `xiaomi-token-plan-*` hosts publish a
thinking toggle.

The distinction matters because `mergeBaseModel` merges the provider
entry over the lab entry, so omitting the field defers to the lab while
writing `false` overrides it. There is no narrower version worth
keeping: when the lab says nothing the inherited value is already
absent, so the rule only ever bites on the routes it gets wrong.

Back to 13 skips. Those six are honest -- the route is skipped because
the catalog contradicts itself, which is the signal that sends the fix
to the endpoint instead of burying it here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gateway publishes two interfaces and they answer different questions.
`/api/v1/models` says a route is reachable here; the canon projection at
`/model-data/index.json` says what AIHubMix has actually verified about
the model behind it. A catalog entry needs the second, so the model list
is now read through canon rather than on its own.

Canon covers 351 models against the list's 409, and the 117 it leaves
out are the long tail the list describes worst: `Qwen/QwQ-32B`, which
has no non-thinking mode, `AiHubmix-Phi-4-mini-reasoning`,
`codex-mini-latest` and the whole `qwen3-*` hybrid-thinking family all
report no `reasoning` flag at all. Reading that silence as a statement
was the mistake the previous commit reverted; this stops the adapter
from having to read it.

Nothing moves today: all 106 creates, all 66 updates and all 66 files
already on disk are covered, so the dry run is unchanged at 106/66. The
filter is a ratchet, not a cleanup — it holds the line when the list
grows a route canon has not reached yet.

Sits in parseModels beside the route-variant filter and drops silently
for the same reason: an uncovered route is not a gap a contributor here
can close, so it should not raise a skip notice or a missing-model
issue. Covered IDs are compared exactly; both registries are generated
from the same gateway catalog and all 292 of today's overlaps match
without case folding.

A failed canon request throws instead of syncing ungated, because
carrying on would publish exactly the routes the gate exists to hold
back. Nothing is written on a throw, and `deleteMissing: false` means a
gated-out route never costs a file either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three review findings, all in how the leading comment block is derived.

The header is authoritative, so `composeHeader` strips any line opening
`# Toggle:`, `# Effort:` or `# Budget:` on the grounds that the derived
block restates it. It only ever derived the toggle. An effort or budget
opening was therefore deleted with nothing put back, leaving the option
row on the file and its field name nowhere -- `deepseek-v4-pro-0813` and
`qwen3.7-flash` each lost one that way. Derive a block per control the
file actually authors, and read the effort levels off the row they
document so the two cannot drift.

The toggle block also named `$.enable_thinking` alone. A control on this
gateway has no single wire path: the same off state is reachable from
whichever SDK dialect the caller speaks. Name one path per protocol,
following what `providers/aihubmix/provider.toml` records for each
surface.

`-reasoning` is the one affix the gateway does not own outright. A lab
can end a model's real name with it, and `AiHubmix-Phi-4-mini-reasoning`
is Microsoft's -- cataloged here as
`providers/azure/models/phi-4-mini-reasoning.toml`. What makes the Grok
routes a steering pair is that they come as a pair, so ask the catalog
for the `-non-reasoning` half rather than trusting the word. `coding-`
needs no such check: 32 routes carry it, every one a relay of a plain
sibling the list also carries, so it joins `-free` as a prefix match and
its five cards go with it.

Two cards carried hand-written data the endpoint contradicts, which is
why the sync kept rewriting them. `deepseek-v4-pro-0813` states effort
`high|max` where both the endpoint and canon publish `low|high|max`;
`qwen3.7-flash` states no effort row at all against seven published
levels, and its `[limit]`/`[modalities]` overrides restate the base
model wrongly -- 991_000 is the input-token ceiling, not the context
window. Both now match the response and survive a second sync unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] providers/aihubmix/models/qwen3.7-flash.toml:22 - Check: Relay reasoning_options must follow lab + same-surface peer baselines (AGENTS.md Reasoning options; audit skill Step 2), not invent a full effort enum. Why: This file replaces first-party/OpenRouter shape (toggle + budget_tokens on alibaba-cn / openrouter) with effort = none|minimal|low|medium|high|xhigh|max plus budget. That is a full-schema dump the skill forbids, and it drops the lab’s on/off control in favor of GPT-style levels the Alibaba path does not use. Action: Author lab/peer-aligned options for this host (at minimum toggle + budget_tokens, plus only effort levels verified on AIHubMix for this model). Update reasoningOptions() in packages/core/src/sync/providers/aihubmix.ts so endpoint lists are intersected with lab/peer baselines (as Eden AI does) instead of pass-through filtering/aliasing alone—otherwise the first automation sync will rewrite Qwen and similar relays the same way.
  • [medium] [possible mistake] providers/aihubmix/models/deepseek-v4-pro-0813.toml:20 - Check: DeepSeek V4 effort baseline is lab high|max (toggle + graded), not an expanded ladder without host evidence. Why: First-party providers/deepseek/models/deepseek-v4-pro.toml is high|max; this PR adds low. OpenRouter peers use low|high|max, so low may be valid on this gateway, but AGENTS still requires host/peer evidence of a meaningful control—not merely that the catalog enum accepts the string. Action: Confirm AIHubMix treats low as a real distinct level for this route (docs or live effect). If it only accepts/aliases like the lab’s high mapping, keep high|max with toggle; if low is real here, cite that in the leading header and keep the OpenRouter-aligned set.

chenxue and others added 2 commits September 15, 2026 19:32
AIHubMix echoes the OpenAI chat protocol's whole `ReasoningEffort` enum for a
route it holds no per-model levels for. The routes it lands on say so: of 409,
18 receive it, among them `grok-4-fast-non-reasoning` — whose name states it
does not reason — with `default = none`, the pinned `gpt-5.2-high`/`-low`
variants, the image route `gemini-3-pro-image`, and every Qwen 3.5/3.6/3.7 entry
while Qwen 3.8 carries a real `low|medium|xhigh`. `default` stays route-specific
throughout, so the host knows the route's setting and is stating the protocol in
`values`.

Live probing agrees. On `qwen3.7-flash` and `deepseek-v4-pro-0813`, only `none`
is observable — it returns no reasoning tokens and an empty `reasoning_content`
— while the six graded levels are reproducibly non-monotonic across two runs
(`minimal` above `high`, `xhigh` lowest). A file copying that would publish a
ladder no caller can steer with.

So the list is dropped where it is exactly the protocol's seven, and the toggle
and budget the same route publishes carry the reasoning surface alone. It is a
shape rule over the response, not a per-model baseline: `qwen3.7-flash` lands on
`toggle` + `budget_tokens`, matching `alibaba-cn` and `openrouter` without either
being consulted, and a narrowed list of any length survives untouched.

`deepseek-v4-pro-0813` keeps the `low|high|max` the host narrows for all seven
DeepSeek routes; its note now records that `low` is not observably distinct and
that `none` works but goes unpublished — both are the host's to fix, and the
file follows what it publishes rather than restating a hand baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The note claimed DeepSeek documents `high|max` with low folded into high.
It documents `Possible values: [low, high, max]`, folding only `medium` and
`xhigh` into `high` (api-docs.deepseek.com, archived in canon 2026-08-13).
`low` is a real published level, so the three values this file carries are the
lab's own enum passed through, not a host-side widening. The single-prompt
probe is kept as what it is — an observation about one prompt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/aihubmix.ts / providers/aihubmix/models/coding-minimax-m2.7-highspeed.toml - Check: Route-variant filtering must not remove a distinct, catalogued host route that has no remaining card. Why: The PR deletes coding-minimax-m2.7-highspeed and parseModels drops every ^coding- ID. That route is not the same as minimax-m2.7 (different price/latency: 0.2/0.2 vs 0.3/1.2), and the commit notes there is no plain minimax-m2.7-highspeed sibling on disk—only uncatalogued cc-/mm- paths. After merge, sync cannot recreate it, so the catalog permanently loses this AIHubMix route. Action: Keep a catalog entry for the highspeed route (exception to the coding- filter, map onto a real listed sibling ID, or add/retain a non-filtered model id), or stop deleting this file until a replacement id is synced.
  • [high] [possible mistake] providers/aihubmix/models/xiaomi-mimo-v2.5.toml / xiaomi-mimo-v2.5-pro.toml - Check: Deleting “alias” files must leave a listed, syncable id for the same model. Why: Patch 1 removes the only non-coding-/non-free MiMo V2.5 cards; later patches also remove coding- and -free variants, and those affixes are filtered out of parseModels. If the public list’s live ids are only the filtered forms (or the xiaomi- ids are truly absent), automation will not restore any MiMo V2.5 entry and the provider loses both models. These files are normal base_model + pricing cards, not obviously hidden aliases like alicloud-* / zai-*. Action: Confirm the LLM list + canon still expose recreatable ids (e.g. xiaomi-mimo-v2.5 / xiaomi-mimo-v2.5-pro). If not, keep the plain files or add an allowlisted mapping before deleting.
  • [medium] [violation] sync.md (AIHubMix Notes) / packages/core/src/sync/providers/aihubmix.ts (missingNotice) - Check: Claims about missing-local handling must match the sync runner. Why: Docs/comments say a local file absent from one response is retained and opens a deduped GitHub issue. The runner only retains when deleteMissing: false and feeds missingLocal into missingNotice notices; issueModels is built from skipped remotes / missing-reasoning / missingRemote only—not retained local paths—so no [missing-model] issue is opened for rotated-out files. Maintainers will not get the automation the note promises. Action: Either wire retained missing locals into the missing-model issue path, or correct sync.md / adapter comments to say notice-only (no GitHub issue).
  • [low] [possible mistake] packages/core/src/sync/providers/aihubmix.ts (buildAihubmixModel shared fields) - Check: Provider-only interleaved should not disappear on first-time creates for reasoners that return a reasoning side channel. Why: interleaved is only copied from existing, so new factored creates never get reasoning_content / true even when current AIHubMix files set it and the gateway exposes that field. Existing files keep it; the large post-merge create set will not. Action: If the host still returns interleaved reasoning for those routes, derive or default interleaved on creates (from endpoint, peers, or a safe host default); don’t rely only on pre-existing files.

…exceptions

`sync.md` said a local file absent from one response is retained *and* opens a
deduped GitHub issue. Only the first half is true: `issueModels` is built from
missing remotes, skipped remotes and missing reasoning options, and a retained
local path is in none of the three, so a rotated-out file surfaces only through
`missingNotice`. State notice-only rather than promise automation that does not
run.

The route-filter block named `minimax-m2.7-highspeed` as an exception without
saying why it is not a gap. It is not: `cc-minimax-m2.7-highspeed` and
`mm-minimax-m2.7-highspeed` are both listed, match no affix in the filter, and
declare `variant_of = minimax-m2.7-highspeed`, which resolves to the lab entry
`models/minimax/MiniMax-M2.7-highspeed.toml` — a dry run against the live list
writes each of them a card, so the filter drops the `coding-` price point and
not the model. The MiMo V2.5 pair reads the same way: the list spells them
`mimo-v2.5`/`mimo-v2.5-pro` with no `xiaomi-` prefix. The one pair that really
goes uncarded is `mimo-v2-omni`/`mimo-v2-pro`, skipped for publishing no
`reasoning_options` rather than by this filter, and that is now said outright.

Comments and docs only; no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@0genlab

0genlab commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Ran the four items down against the live listing and a real dry run. Two of the four hold; the two [high] ones do not. Evidence below, and a0539cae7 lands the two that do.

bun ./packages/core/script/sync-models.ts aihubmix --dry-run (against the live endpoint, 409 routes):

Would create mm-minimax-m2.7-highspeed.toml
Would create cc-minimax-m2.7-highspeed.toml
Would create cc-minimax-m2.7.toml
Would create mimo-v2.5.toml
Would create mimo-v2.5-pro.toml
Would create glm-5.1.toml
Would create deepseek-v4-flash.toml
Would create deepseek-v4-pro.toml
Dry run: 97 created, 59 updated, 0 removed, 2 unchanged

[high] coding-minimax-m2.7-highspeed — not a loss. The cc-/mm- routes are not "uncatalogued"; they are simply not in the repo yet. cc-minimax-m2.7-highspeed and mm-minimax-m2.7-highspeed are both on the live list, neither matches ^coding- nor the -free/-non-reasoning suffixes, and both declare variant_of = minimax-m2.7-highspeed, which resolves to the lab entry models/minimax/MiniMax-M2.7-highspeed.toml — present in the repo. The dry run writes each a card. What the filter drops is the coding- price point (input 0.2 against 0.1 on cc-/mm-), which is the same thing -free does and, by this adapter's stated rule, is not what makes an entry its own model. The comment that named the exception without explaining it is rewritten in a0539cae7.

[high] MiMo V2.5 — not a loss. The live list carries no xiaomi- prefixed MiMo at all; the IDs are mimo-v2.5 and mimo-v2.5-pro, and the dry run creates both. Full audit of all 17 deletions in this PR: every one has a listed, syncable replacement (deepseek-v4-flash, deepseek-v4-pro, glm-5.1, minimax-m2.7, mimo-v2.5, mimo-v2.5-pro, cc-/mm-minimax-m2.7-highspeed). Zero gaps.

[medium] sync.md missing-local — correct, fixed. issueModels is built from missing remotes, skipped remotes and missing reasoning options; a retained local path is in none of the three, so no [missing-model] issue is opened for a rotated-out file. The note now says notice-only. (The missingNotice text itself was already accurate — only the sync.md clause overclaimed.)

[low] interleaved on creates — real gap, but not one this adapter can close. There is no source to derive it from: the endpoint publishes no interleaved surface, and ModelMetadata has no interleaved key at all (it lives only on ModelBase), so a base_model entry cannot supply one either. Filling it from peer files or a host default would be inventing data, which is what the endpoint-driven rule here exists to prevent. It sits in the same class as temperature, experimental and provider, already documented in sync.md as fields the endpoint has no surface for. Raised upstream with AIHubMix instead.

The one pair that genuinely goes uncarded is mimo-v2-omni/mimo-v2-pro — listed as reasoning while publishing no reasoning_options, so skipped by that rule rather than by the route filter. That is now said outright in the filter comment, and it too is an endpoint fix.

@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [possible mistake] packages/core/src/sync/providers/aihubmix.ts (statesProtocolNotModel / effort filter) - Check: Relay reasoning_options must reflect this host’s real per-model controls (lab + same-surface peers), not a shape heuristic that drops legitimate ladders. Why: Dropping any effort list that is exactly none|minimal|low|medium|high|xhigh|max treats the OpenAI chat enum as always non-model-specific. That can strip real GPT-style ladders, leave only toggle/budget, or yield reasoning_options = undefined and skip/create-fail reasoners that only publish that set. Live non-monotonic probes on Qwen/DeepSeek do not prove the same for every route that carries those seven values. Action: Scope the drop to routes where the host is known to echo the protocol (or require a second signal), and keep full seven-value lists when they match lab/peer GPT (or equivalent) baselines on this host.
  • [medium] [possible mistake] providers/aihubmix/models/deepseek-v4-pro-0813.toml - Check: DeepSeek V4 Pro effort baseline is lab + peers (AGENTS.md: Pro → toggle + high/max; first-party providers/deepseek/models/deepseek-v4-pro.toml is high/max). Why: This file now authors low|high|max and claims those three are the lab’s published Pro set. That conflicts with the first-party Pro entry (high/max only; Flash is the line that uses low|high|max). Publishing low on Pro can invent a graded level callers should not rely on. Action: Align with first-party Pro (and peers that match Pro), or cite host-specific evidence that AIHubMix’s Pro route truly exposes a distinct low and update the note so it does not claim the lab Pro enum is low|high|max if the lab Pro catalog does not.
  • [low] [violation] sync.md (AIHubMix Notes) - Check: New sync providers must document source-of-truth behavior for maintainers (sync.md / Adding A Provider). Why: The adapter gates the catalog on https://aihubmix.com/model-data/index.json (env AIHUBMIX_CANON_URL), throws if that fetch fails, and silently omits uncovered routes, but the AIHubMix section never mentions the canon gate—only the models list. Action: Document the canon endpoint, exact ID matching, fail-closed fetch, and silent omission of uncovered routes next to the other AIHubMix rules.

…n over

The endpoint now states `interleaved` per wire protocol — `true` where the
channel exists but its carrier has no settled name, `{field}` where it does —
because the carrier is a property of the protocol shape rather than of the
model: `claude-opus-4-8` returns thinking blocks on `/v1/messages` and nothing
at all on the chat-completions path.

Which protocol a given model is spoken over is itself per-model, not per
provider: `@aihubmix/ai-sdk-provider` builds `claude-*` as an Anthropic
messages model, `gemini*`/`imagen*` as a Google generative model (except the
`-nothink`/`-search` routes, which it sends back down the OpenAI-compatible
path), and everything else as an OpenAI-compatible chat model. `wireProtocol`
transcribes that from `createChatModel`, so each model is read on its own face
and never on another model's.

Silence stays unknown rather than denial, the same reading the missing
`reasoning` flag gets: a model the endpoint says nothing about keeps whatever
the file authored, and a face the endpoint describes for other models is no
less silent about this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] providers/aihubmix/models/deepseek-v4-pro-0813.toml:13 - Check: Relay reasoning_options must match the lab / same-surface peer baseline for that model (AGENTS.md → Reasoning options; audit skill baseline = lab + peers). Why: This PR changes the route from toggle + high|max to toggle + low|high|max. First-party providers/deepseek/models/deepseek-v4-pro.toml (same base_model = deepseek/deepseek-v4-pro-0813) authors high|max only. The file’s own note still records that a live probe could not separate low/high/max on this host. Publishing low invents a graded level the lab Pro surface and peers do not treat as distinct. Action: Keep values = ["high", "max"] (or other verified host-true set), and stop the adapter from writing a wider AIHubMix effort list over that lab baseline for DeepSeek V4 Pro routes unless this host documents a real, observable low level.

@github-actions github-actions Bot added the reviewer: ready Automated review found no actionable items label Sep 16, 2026
@0genlab

0genlab commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

The [high] on deepseek-v4-pro-0813.toml:13 rests on providers/deepseek/models/deepseek-v4-pro.toml (high|max) being the lab baseline. That file is stale, and the restriction it encodes has been withdrawn by the lab. Evidence, rechecked against both DeepSeek pages today:

1. reasoning_effort on the chat API reference (https://api-docs.deepseek.com/api/create-chat-completion)

Accepted values are none, low, high, max. "The default effort is high." none disables thinking mode; low / high / max enable it. minimal is accepted and mapped to low; medium / xhigh are accepted and mapped to high. The page lists both deepseek-flash and deepseek-v4-pro and draws no per-model distinction on this field. Nothing on the page maps low to high.

2. The effort mapping table (https://api-docs.deepseek.com/guides/thinking_mode)

There is one table, and it is not broken down per model:

Requested Actual
minimal low
low low
medium high
high high
xhigh high
max max
ultra max

Its only two footnotes are "(1) Thinking mode is enabled by default, with the default effort being high" and "(2) The mapping between the effort set by the user and the model's actual reasoning effort is as follows". There is no pro-specific row, no pro-specific footnote, and deepseek-v4-pro is not mentioned on the page at all. low maps to low — it is its own level.

3. Where the stale baseline came from

$ git log -1 --format='%ad  %s' --date=short -- providers/deepseek/models/deepseek-v4-pro.toml
2026-08-13  Add DeepSeek V4 Pro 0813 (#4570)

DeepSeek's docs used to carry a sentence restricting pro to high and max only. That sentence is no longer on either page. The first-party file was authored while it was still there and has not been touched since — it is a snapshot of a restriction the lab has removed.

4. The repo already disagrees with itself

providers/deepseek/models/deepseek-v4-flash.toml             values = ["low", "high", "max"]
providers/deepseek/models/deepseek-v4-flash-vision-exp.toml  values = ["low", "high", "max"]
providers/deepseek/models/deepseek-v4-pro.toml               values = ["high", "max"]

while AGENTS.md:202 states the rule for the whole line:

DeepSeek V4 → toggle + high / max (not L/M/H; lab maps low/medium→high)

That rule contradicts the two flash files sitting next to it, and contradicts the current mapping table (low → low, medium → high). The flash entries tracked the doc change; the pro entry and the AGENTS rule did not.

So values = ["low", "high", "max"] on the AIHubMix route is the lab-aligned set, not a widening of it. Reverting it to ["high", "max"] would republish a per-model restriction the lab has withdrawn, and would keep this route inconsistent with its own sibling flash routes.

On the probe note in the header. The file records that a 2026-09-15 probe could not separate low/high/max on a single prompt (522/564/510 reasoning tokens, non-monotonic). That was never offered as evidence that low exists — it is a statement about that prompt. One prompt failing to separate three levels does not bear on whether the lab defines them, and the mapping table defines low explicitly.

One correction on our side. The header line "Off is the toggle on this path; none is a level only on the Responses face" is inaccurate: the chat page does list none among reasoning_effort values, as the off switch. The effort values stay low|high|max because on/off is carried by the separate toggle entry, but the parenthetical should say that rather than what it currently says. Happy to fix the wording.

If maintainers want, I can also bring AGENTS.md:202 and providers/deepseek/models/deepseek-v4-pro.toml in line with the current docs — either in this PR or a separate one, whichever you prefer for review.

Resolves the only conflict, in packages/core/src/sync/index.ts: dev added
the `aiand` provider (anomalyco#7040) at the head of the alphabetical registry while
this branch added `aihubmix` in the same position. Both are kept, ordered
`aiand` then `aihubmix`, across all three sites (import, the providers type,
the providers object). No logic is involved — it is an insertion-order
conflict.

Checked the parts that merged cleanly but could still drift: `groups` is new
on dev, and `aihubmix` is in `aggregators` where a gateway belongs, not in
`direct`. sync.test.ts and sync.md merged without conflict.

Tests on the merge result: 232 pass / 3 fail. The same 3 fail on a clean
origin/dev checkout (Hyper reasoning inheritance, DeepInfra live modalities,
LLM Gateway case variant), as do the tsc errors in sync.test.ts, so both are
pre-existing on dev and not introduced here. Base has 194 pass / 3 fail, so
this branch adds 38 passing tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the reviewer: ready Automated review found no actionable items label Sep 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No actionable findings.

@github-actions github-actions Bot added the reviewer: ready Automated review found no actionable items label Sep 18, 2026
The header claimed `none` was "a level only on the Responses face". It is not:
DeepSeek's chat-completions docs list it in the same set
("Possible values: [none, low, high, max]") and describe it as disabling
thinking mode, i.e. an off switch spelled as a level rather than a fourth
intensity. Off is already carried by `type = "toggle"`, so `none` stays out of
the effort values -- the data was right, the explanation was not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the reviewer: ready Automated review found no actionable items label Sep 18, 2026
The previous run failed on an upstream 402 ("Insufficient account funds"
from opencode.ai/zen), not on anything in this PR, and the workflow only
fires on opened/reopened/synchronize/ready_for_review -- there is no
retry. Empty commit, no content change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/index.ts:111 - Check: context.header / authoritativeHeaders note preservation. Why: The AIHubMix adapter sets authoritativeHeaders: true and calls composeHeader(context.header?.(model.model_id), …) so hand-authored notes (pricing citations, live-test lines) survive rewrites. The SyncProvider context type never exposes header, and syncProvider never passes the on-disk leading comment into translateModel—only existing and authored. On every update with a derived reasoning header, context.header is always missing, so composeHeader cannot keep notes; authoritativeHeaders then replaces the file header with the derived block alone. That drops verified notes on routes such as deepseek-v4-pro-0813 and contradicts the adapter’s own tests/docs, which assume a working header callback. Action: Wire the existing file header into translateModel (extend the context type and pass existing.get(...).header from the runner), or stop relying on context.header and preserve notes another way that actually runs in production sync.
  • [medium] [possible mistake] providers/aihubmix/models/deepseek-v4-pro-0813.toml:1 - Check: Reasoning effort baseline for DeepSeek V4 Pro on a multi-model relay. Why: This PR changes the route from lab/peer-aligned high|max to low|high|max. First-party providers/deepseek/models/deepseek-v4-pro.toml and providers/deepseek/provider.toml treat Pro as high|max with low folded into high. Publishing a distinct low on AIHubMix can misstate caller controls unless this host actually exposes a separate low. Action: Confirm AIHubMix’s published/live surface for this ID really distinguishes low from high; if not, keep ["high", "max"] (plus toggle) to match the lab baseline.

Every AIHubMix test built its own translateModel context, so the
`header` callback could have been wired nowhere and the suite would
still be green. The new test drives the real runner against local
fixtures: a hand-written note on disk has to survive a sync that
rewrites the header block it sits under.

It lives in its own file because a real sync installs module-level
catalog state the other AIHubMix tests expect unset.

Writing it surfaced a duplicate-modality bug: when the endpoint omits
modalities, the fallback is two overlapping records -- the narrowing
list already on the file and the lab entry it narrows -- and a shared
entry was written twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@0genlab

0genlab commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — the [medium] is a fair thing to ask about and I've answered it below. The [high] is a false positive, and I think I know why.

[high]context.header is wired, by this PR

The review asks me to "extend the context type and pass existing.get(...).header from the runner". That is exactly what the diff does. git diff origin/dev...HEAD -- packages/core/src/sync/index.ts:

@@ -113,6 +119,12 @@ export interface SyncProvider<SourceModel> {
       existing(id: string): ExistingModel | undefined;
       authored(id: string): ExistingModel | undefined;
+      /**
+       * The leading comment block already on the file, so a provider that owns
+       * its header (authoritativeHeaders) can refresh the part it generates
+       * without discarding notes a human wrote around it.
+       */
+      header?(id: string): string | undefined;
     },
@@ -283,6 +298,9 @@ export async function syncProvider<SourceModel>(
         authored(id) {
           return existing.get(`${id}.toml`)?.authored;
         },
+        header(id) {
+          return existing.get(`${id}.toml`)?.header || undefined;
+        },

The key shape matches the write side: relativePath is ${translated.id}.toml, and translateModel is called with the same bare id the callback re-suffixes.

My guess at the cause: the workflow checks out the trusted base revision and hands the agent .pr-review/diff.patch. Reading index.ts off the base tree would show precisely the absence the review describes.

But the review found a real gap, and it's now closed

Nothing in the suite actually exercised that wiring. Every AIHubMix test constructs the translateModel context itself, so header could have been passed nowhere and the tests would still be green — the bug was unreachable by the tests, not by the code. Fixed in a0823d3: packages/core/test/sync-aihubmix-runner.test.ts drives the real syncProvider against local fixtures (AIHUBMIX_MODELS_URL / AIHUBMIX_CANON_URL accept file://), writes a file carrying a hand-written verification note under a stale header, syncs, and asserts the note survives while the derived block is replaced.

I verified the test is load-bearing by deleting the three runner lines above and re-running: it fails with the note dropped and the header reduced to the derived block alone — i.e. it reproduces the exact failure mode the review described. Restored, it passes.

It lives in its own file because a real sync installs module-level catalog state (lab IDs, relay listing, canon cover) that the other AIHubMix tests expect unset; inlining it into sync.test.ts broke two unrelated tests.

Writing it also surfaced a genuine one-line bug in this PR, which I've fixed in the same commit: when the endpoint omits modalities for a route, modalities() returned the un-deduped fallback, and since that fallback concatenates two overlapping records — the narrowing list already on the file and the lab entry it narrows — a shared entry was emitted twice (input = ["text", "text"]).

[medium]low on deepseek-v4-pro-0813

Straight answer first: I have no behavioural evidence that low produces different output from high on this host. A probe on 2026-09-15 gave 522 / 564 / 510 reasoning tokens for low / high / max on a single prompt — non-monotonic, i.e. it separates nothing. That result is written into the file header rather than hidden, precisely because it is not a discriminator.

The claim rests on something else: in models.dev, effort values records the declared accepted domain, not a measured intensity gradient. DeepSeek documents reasoning_effort as Possible values: [none, low, high, max] with minimal→low and medium/xhigh folded into high (api-docs.deepseek.com, checked 2026-09-18). low is a first-class accepted value there; the relay passes the field through unchanged. Recording high|max would mean asserting the host rejects or ignores low, which is a stronger claim than I can support and is contradicted by the upstream docs.

On the divergence from providers/deepseek/models/deepseek-v4-pro.toml: that file says high|max and by the same reading is the one that's short a value. I deliberately did not touch it in this PR — it's a first-party provider file outside this PR's scope, and a relay PR is the wrong place to relitigate it. Happy to open a separate PR against it if maintainers agree with the reading.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

No actionable findings.

@github-actions github-actions Bot added reviewer: ready Automated review found no actionable items and removed reviewer: ready Automated review found no actionable items labels Sep 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No actionable findings.

@github-actions github-actions Bot added the reviewer: ready Automated review found no actionable items label Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

reviewer: ready Automated review found no actionable items

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant